Rustでライフゲームを作ってみた

Rustでライフゲームを作ってみた

Clock Icon2024.11.15

ゲームソリューション部の えがわ です。

今回はRustをとりあえず触ってみる目的でライフゲームを作成してみました。

ライフゲームとは?

グリッド状のマス目にセル(細胞)を配置し、いくつかの簡単なルールに従って世代を繰り返していきます。
セルは「生きている」か「死んでいる」かの2状態があり、周囲のセルに影響されながら新たな形を生み出します。
見るだけで楽しいアニメーションが作れたり、予測不能なパターンが生まれるので、プログラミング勉強での面白い題材となっています。

完成系

lifegame_rust_2

ライフゲームのルール

ライフゲームはルールが決まっています。

  • 誕生:死んでいるセルの周囲にちょうど3つの生きたセルがあれば、そのセルは次の世代で「生」へと変わります。
  • 生存:生きているセルの周囲に2つまたは3つの生きたセルがあれば、そのセルは次の世代でも「生」のままです。
  • 過疎:生きているセルの周囲に1つ以下の生きたセルしかない場合、そのセルは次の世代で「死」に変わります。これは過疎による死です。
  • 過密:生きているセルの周囲に4つ以上の生きたセルがある場合、そのセルは次の世代で「死」に変わります。これは過密による死です。

環境

  • Ubuntu 22.04.4 LTS(WSL2)
  • cargo 1.82.0
  • rustc 1.82.0

環境構築

Rustを実行するための環境構築を行います。

Rustのインストール

rustup を使用してRustをインストールします。

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

プロンプトが表示されたら、default のオプション(1 番)を選択してRustをインストールします。

.cargo/bin が PATH に追加されるので、インストールが適切に行われたか確認します。

source $HOME/.cargo/env
rustc --version

正常にインストールされていれば、Rustコンパイラのバージョンが表示されます。

プロジェクトの作成

以下のコマンドでプロジェクトを作成、そして初期プロジェクトを起動してみます。

cargo new lifegame_rust
cd lifegame_rust/
cargo build
cargo run

Hello, world!が表示されれば環境構築は完了です。

ライフゲームを作成

プログラムの全貌はこちら

main.rs
use std::time::Duration;
use std::{thread, io};

const WIDTH: usize = 80;
const HEIGHT: usize = 40;

fn main() {
    let mut grid = vec![vec![false; WIDTH]; HEIGHT];

    // Glider
    grid[1][2] = true;
    grid[2][3] = true;
    grid[3][1] = true;
    grid[3][2] = true;
    grid[3][3] = true;

    // Glider Gun
    let glider_gun_coords = [
        (5, 1), (5, 2), (6, 1), (6, 2), (3, 13), (3, 14), (4, 12), (4, 16), (5, 11), (5, 17),
        (6, 11), (6, 15), (6, 17), (6, 18), (7, 11), (7, 17), (8, 12), (8, 16), (9, 13), (9, 14),
        (1, 25), (2, 23), (2, 25), (3, 21), (3, 22), (4, 21), (4, 22), (5, 21), (5, 22), (6, 23),
        (6, 25), (7, 25), (3, 35), (3, 36), (4, 35), (4, 36),
    ];
    for &(x, y) in &glider_gun_coords {
        grid[x][y] = true;
    }

    // LWSS
    let lwss_coords = [
        (20, 20), (21, 20), (22, 20), (23, 20), (20, 23), (23, 23), (23, 21), (23, 22), (21, 24),
        (22, 24)
    ];
    for &(x, y) in &lwss_coords {
        grid[y][x] = true;
    }

    // Pulsar
    let pulsar_coords = [
        (25, 25), (26, 25), (27, 25), (33, 25), (34, 25), (35, 25),
        (23, 27), (28, 27), (30, 27), (22, 27), (31, 27), (23, 28),
        (28, 28), (30, 28), (22, 28), (31, 28), (22, 29), (31, 29),
        (22, 30), (31, 30), (22, 32), (23, 32), (28, 32), (30, 32),
        (31, 32), (22, 33), (23, 33), (28, 33), (30, 33), (31, 33),
        (25, 35), (26, 35), (27, 35), (33, 35), (34, 35), (35, 35),
    ];
    for &(x, y) in &pulsar_coords {
        grid[y][x] = true;
    }

    loop {
        print_grid(&grid);
        grid = next_generation(&grid);
        thread::sleep(Duration::from_millis(100));
        clear_screen();
    }
}

fn next_generation(grid: &Vec<Vec<bool>>) -> Vec<Vec<bool>> {
    let mut new_grid = vec![vec![false; WIDTH]; HEIGHT];

    for y in 0..HEIGHT {
        for x in 0..WIDTH {
            let alive_neighbors = count_alive_neighbors(grid, x, y);

            if grid[y][x] {
                // 生きているセル
                if alive_neighbors == 2 || alive_neighbors == 3 {
                    new_grid[y][x] = true;
                }
            } else {
                // 死んでいるセル
                if alive_neighbors == 3 {
                    new_grid[y][x] = true;
                }
            }
        }
    }

    new_grid
}

fn count_alive_neighbors(grid: &Vec<Vec<bool>>, x: usize, y: usize) -> usize {
    let mut count = 0;

    for dy in -1..=1 {
        for dx in -1..=1 {
            if dx == 0 && dy == 0 {
                continue;
            }

            let nx = (x as isize + dx) as usize;
            let ny = (y as isize + dy) as usize;

            if nx < WIDTH && ny < HEIGHT && grid[ny][nx] {
                count += 1;
            }
        }
    }

    count
}

fn print_grid(grid: &Vec<Vec<bool>>) {
    let mut output = String::new();

    for row in grid {
        for &cell in row {
            if cell {
                output.push('■');
            } else {
                output.push(' ');
            }
        }
        output.push('\n');
    }

    println!("{}", output);
}

fn clear_screen() {
    print!("{}[2J", 27 as char);
    print!("{}[1;1H", 27 as char);
}

初期配置

ライフゲームにはいくつかの有名な初期配置があり、それぞれに名前が付けられています。

グライダー

45度の角度で対角線上に移動する小さなパターン

// Glider
grid[1][2] = true;
grid[2][3] = true;
grid[3][1] = true;
grid[3][2] = true;
grid[3][3] = true;

グライダーガン

定期的にグライダーを生成するパターン

// Glider Gun
let glider_gun_coords = [
    (5, 1), (5, 2), (6, 1), (6, 2), (3, 13), (3, 14), (4, 12), (4, 16), (5, 11), (5, 17),
    (6, 11), (6, 15), (6, 17), (6, 18), (7, 11), (7, 17), (8, 12), (8, 16), (9, 13), (9, 14),
    (1, 25), (2, 23), (2, 25), (3, 21), (3, 22), (4, 21), (4, 22), (5, 21), (5, 22), (6, 23),
    (6, 25), (7, 25), (3, 35), (3, 36), (4, 35), (4, 36),
];

ライトウェイトスペースシップ (Light-weight spaceship, LWSS)

4x5のセルで構成される移動パターン

// LWSS
let lwss_coords = [
    (20, 20), (21, 20), (22, 20), (23, 20), (20, 23), (23, 23), (23, 21), (23, 22), (21, 24),
    (22, 24)
];

パルサー (Pulsar)

3x3ブロックが4つの位置にあり、その間を3x1ブロックが充填されています。

// Pulsar
let pulsar_coords = [
    (25, 25), (26, 25), (27, 25), (33, 25), (34, 25), (35, 25),
    (23, 27), (28, 27), (30, 27), (22, 27), (31, 27), (23, 28),
    (28, 28), (30, 28), (22, 28), (31, 28), (22, 29), (31, 29),
    (22, 30), (31, 30), (22, 32), (23, 32), (28, 32), (30, 32),
    (31, 32), (22, 33), (23, 33), (28, 33), (30, 33), (31, 33),
    (25, 35), (26, 35), (27, 35), (33, 35), (34, 35), (35, 35),
];

処理詳細

周囲のセル数を取得

セルの周囲の生きているセル数を取得しています。

fn count_alive_neighbors(grid: &Vec<Vec<bool>>, x: usize, y: usize) -> usize {
    let mut count = 0;

    for dy in -1..=1 {
        for dx in -1..=1 {
            if dx == 0 && dy == 0 {
                continue;
            }

            let nx = (x as isize + dx) as usize;
            let ny = (y as isize + dy) as usize;

            if nx < WIDTH && ny < HEIGHT && grid[ny][nx] {
                count += 1;
            }
        }
    }

    count
}

次の世代を生成

次の世代を生成するためにGridを再生成しています。
誕生 or 生存セルのみtrueを代入しています。

fn next_generation(grid: &Vec<Vec<bool>>) -> Vec<Vec<bool>> {
    let mut new_grid = vec![vec![false; WIDTH]; HEIGHT];

    for y in 0..HEIGHT {
        for x in 0..WIDTH {
            let alive_neighbors = count_alive_neighbors(grid, x, y);

            if grid[y][x] {
                // 生きているセル
                if alive_neighbors == 2 || alive_neighbors == 3 {
                    new_grid[y][x] = true;
                }
            } else {
                // 死んでいるセル
                if alive_neighbors == 3 {
                    new_grid[y][x] = true;
                }
            }
        }
    }

    new_grid
}

さいごに

Rustを使用してライフゲームを作成してみました。
ライフゲームはコンソール画面で楽しむことができるので、プログラムの勉強として、そして、ゲーム制作のとっかかりとして、とても適していると思いました。
この記事がどなたかの参考になれば幸いです。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.